Skip to content

refactor(v3.0 Part B/2): add correct structure — Utilities/, VoiceServer/, PAI root docs, voice ID fix#83

Merged
Steffen025 merged 8 commits intodevfrom
feature/v3.0-part-b
Mar 20, 2026
Merged

refactor(v3.0 Part B/2): add correct structure — Utilities/, VoiceServer/, PAI root docs, voice ID fix#83
Steffen025 merged 8 commits intodevfrom
feature/v3.0-part-b

Conversation

@Steffen025
Copy link
Owner

@Steffen025 Steffen025 commented Mar 20, 2026

Summary

This is Part 2 of 2 of the v3.0 structure cleanup (split from #81 for CodeRabbit review — max ~100 files per PR).

Stacked on: #82 (Part A — must merge first)


What & Why (6 commits, 74 files)

1. Move orphan skills into Utilities/ category (44 files — renames)

Skills that had no category home were moved into the new Utilities/ grouping:

  • AudioEditor, CodeReview, OpenCodeSystem, Sales, System, WriteStory

2. Add missing PAI/ root docs from 4.0.3 (3 files)

  • PIPELINES.md — pipeline documentation
  • doc-dependencies.json — dependency graph
  • THEPLUGINSYSTEM.md — restored from git history (ADR-001: Hooks → Plugins)

3. Rename voice-server/VoiceServer/ + add 8 missing files (13 files)

CamelCase rename to match PAI 4.0.3 convention, plus missing shell scripts and configs:
install.sh, restart.sh, start.sh, stop.sh, status.sh, uninstall.sh, pronunciations.json, voices.json, menubar scripts

4. Restore missing files + remove legacy content (8 files)

  • Add PAISECURITYSYSTEM/HOOKS.md
  • Add .opencode/PAI/Tools/ scripts: GenerateSkillIndex.ts, SkillSearch.ts, ValidateSkillStructure.ts
  • Remove leftover legacy docs from skills/PAI/ deletion

5. Remove internal planning docs (3 files — deletions)

  • .prd/PRD-20260309-coderabbit-pr47-fixes.md
  • .prd/PRD-20260309-installer-refactor.md
  • docs/architecture/INSTALLER-REFACTOR-PLAN.md

6. Fix hardcoded ElevenLabs voice IDs → dynamic routing (5 files)

Replace hardcoded fTtv3eikoepIosk8dTZ5 with voice_id:"default" + title:"AgentName".
VoiceServer resolves dynamically via TTS_PROVIDER env var.
Files: PAI/SKILL.md, PAI/Algorithm/v3.7.0.md, PAI/Tools/algorithm.ts, VoiceServer/server.ts, agents/Algorithm.md


Files Changed: 74

  • 44 renames (Utilities/ reorganization)
  • 27 additions (new docs, scripts, configs, restored files)
  • 3 deletions (planning docs cleanup)

Related

Summary by CodeRabbit

  • New Features

    • Voice server install/start/stop/restart/uninstall scripts plus menubar plugin and health/test actions
    • Voice profile catalog added with selectable profiles and tuning parameters
    • Skill tools: index generation, CLI search, and structure validation utilities
  • Documentation

    • New Pipelines, Plugin System, Hook/System, and Security hook docs; updated documentation index
  • Configuration

    • Pronunciation overrides added; example payloads and docs now use voice_id: "default"

@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Warning

Rate limit exceeded

@Steffen025 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d047fa6a-84e2-4c3b-96cc-748f8ccdd1be

📥 Commits

Reviewing files that changed from the base of the PR and between 5655797 and 2612021.

📒 Files selected for processing (4)
  • .opencode/PAI/Tools/GenerateSkillIndex.ts
  • .opencode/PAI/doc-dependencies.json
  • .opencode/PAISECURITYSYSTEM/ARCHITECTURE.md
  • .opencode/PAISECURITYSYSTEM/README.md
📝 Walkthrough

Walkthrough

This PR adds extensive PAI 4.0 documentation (Pipelines, Plugin and Hook systems), new VoiceServer install/management scripts and configs, skill-indexing and validation tooling, reorganizes skills into hierarchical categories, and standardizes voice notifications to use a "default" voice profile.

Changes

Cohort / File(s) Summary
PAI docs (voice payload & index)
/.opencode/PAI/Algorithm/v3.7.0.md, /.opencode/PAI/SKILL.md, /.opencode/PAI/DOCUMENTATIONINDEX.md
Replaced concrete voice_id with "default" in example curl payloads and updated documentation index reference from THEHOOKSYSTEM → THEPLUGINSYSTEM.
PAI concept docs
/.opencode/PAI/PIPELINES.md, /.opencode/PAI/THEHOOKSYSTEM.md, /.opencode/PAI/THEPLUGINSYSTEM.md, /.opencode/PAISECURITYSYSTEM/HOOKS.md
Added Pipelines primitive and comprehensive Hook/Plugin/Security documentation describing events, handlers, patterns, and examples.
Skill tooling
/.opencode/PAI/Tools/GenerateSkillIndex.ts, /.opencode/PAI/Tools/SkillSearch.ts, /.opencode/PAI/Tools/ValidateSkillStructure.ts
Added three Bun/TypeScript CLI tools: index generation, CLI search with relevance scoring, and repository skill-structure validation (symlink-safe, frontmatter checks, depth rules).
VoiceServer lifecycle scripts
/.opencode/VoiceServer/install.sh, /.opencode/VoiceServer/start.sh, /.opencode/VoiceServer/stop.sh, /.opencode/VoiceServer/restart.sh, /.opencode/VoiceServer/status.sh, /.opencode/VoiceServer/uninstall.sh
Added macOS LaunchAgent-based install/start/stop/restart/status/uninstall scripts with health checks, logs, ElevenLabs fallback handling, and user prompts.
VoiceServer menubar
/.opencode/VoiceServer/menubar/install-menubar.sh, /.opencode/VoiceServer/menubar/pai-voice.5s.sh
Added SwiftBar/BitBar installer and a 5s-refresh menubar plugin to show server status and quick actions.
Voice configuration & pronunciations
/.opencode/VoiceServer/voices.json, /.opencode/VoiceServer/pronunciations.json, /.opencode/PAI/Tools/algorithm.ts
Added centralized voice profiles (including default) and pronunciation overrides; changed local VOICE_ID constant to "default".
VoiceServer runtime change
/.opencode/VoiceServer/server.ts
sendNotification now prefers voiceConfig?.voice_name and falls back to safeTitle (affects voice resolution when voice_name absent).
Docs metadata & skill-index update
/.opencode/PAI/doc-dependencies.json, /.opencode/agents/Algorithm.md, /.opencode/skills/skill-index.json
Added doc-dependencies manifest; updated agent Algorithm front-matter to use default voice; regenerated skill-index.json with several skills moved into Utilities/ category and counts updated (totalSkills 54→52, flat→hierarchical changes).
Removed planning docs
/.prd/PRD-20260309-coderabbit-pr47-fixes.md, /.prd/PRD-20260309-installer-refactor.md, docs/architecture/INSTALLER-REFACTOR-PLAN.md
Deleted three PRD/architecture planning documents (content removed).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰
I hopped through docs and scripts tonight,
Launched a server, set the voices right,
Indexed skills in tidy rows,
Pipelines hum where workflow flows,
Default voice sings — a rabbit’s delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main structural changes: moving skills to Utilities/, restoring PAI docs, renaming VoiceServer/, and fixing voice ID configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/v3.0-part-b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity:
- AudioEditor → Utilities/AudioEditor (was missing from Utilities)
- CodeReview → Utilities/CodeReview (OpenCode-specific)
- OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific)
- Sales → Utilities/Sales (OpenCode-specific)
- System → Utilities/System (OpenCode-specific)
- WriteStory → Utilities/WriteStory (OpenCode-specific)

OpenCode-specific skills preserved in Utilities/ category.
Added 3 files missing from PAI/ root:
- doc-dependencies.json (documentation dependency graph)
- PIPELINES.md (pipeline system documentation)
- THEHOOKSYSTEM.md (hooks/plugin system documentation)

All .claude/ references replaced with .opencode/.
Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3)

Added missing files from PAI 4.0.3:
- install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh
- voices.json, pronunciations.json
- menubar/ directory

All .claude/ references replaced with .opencode/ in copied files.
…eletion

Restored from git history (accidentally deleted with skills/PAI/):
- PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md)
- PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts
- PAI/PIPELINES.md, PAI/doc-dependencies.json
- PAISECURITYSYSTEM/HOOKS.md

Removed legacy files NOT in 4.0.3 upstream:
- PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md,
  SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts)
- PAI/UPDATES/ directory (not in 4.0.3)
- PAI/Workflows/ (11 legacy workflows not in 4.0.3 root)
- PAI/USER/ reset to 4.0.3 template (README.md placeholders only,
  removed 47 personal template files that don't belong in public repo)

Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md

Regenerated skill-index.json (52 skills, 7 categories)

PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations:
- THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001)
- MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C)
VoiceServer sendNotification() now uses title (agent name) instead of
voiceId for voice resolution. This enables dynamic provider switching
via TTS_PROVIDER env var (google/elevenlabs/macos).

Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName'

Files affected:
- VoiceServer/server.ts (line 425: voiceId → safeTitle)
- PAI/SKILL.md (7 phase curls)
- PAI/Algorithm/v3.7.0.md (2 voice curls)
- agents/Algorithm.md (frontmatter + 3 references)
- PAI/Tools/algorithm.ts (VOICE_ID constant)

Zero hardcoded ElevenLabs IDs remain in the codebase.
@Steffen025 Steffen025 force-pushed the feature/v3.0-part-b branch from 1d85d4a to 2bd381c Compare March 20, 2026 14:53
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (19)
.opencode/VoiceServer/stop.sh-31-35 (1)

31-35: ⚠️ Potential issue | 🟠 Major

Do not kill arbitrary listeners on port 8888.

This cleanup will SIGKILL any process bound to :8888, even if the LaunchAgent failed and the port belongs to another app. That makes a normal stop capable of taking down unrelated local services; the same block is duplicated in .opencode/VoiceServer/uninstall.sh:55-58.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/stop.sh around lines 31 - 35, The script currently
SIGKILLs any process on :8888 which can kill unrelated apps; change the cleanup
to only stop the VoiceServer LaunchAgent process by either (a) reading and using
a stored PID file (e.g., create/use a pidfile written when starting the server)
or (b) resolving PIDs from lsof -ti :8888 and verifying each PID's command line
via ps -p <pid> -o args= contains a known identifier (e.g., the
VoiceServer/LaunchAgent job label or the server binary name) before sending a
kill; apply the same guarded logic to the duplicate block in uninstall.sh so
only matching processes are terminated.
.opencode/VoiceServer/uninstall.sh-36-43 (1)

36-43: ⚠️ Potential issue | 🟠 Major

Check launchctl unload before claiming the service is stopped.

Unlike .opencode/VoiceServer/stop.sh:21-25, this path prints success unconditionally. If launchctl unload fails, the uninstall continues with a loaded LaunchAgent and ends with a false Uninstall Complete state.

🐛 Proposed fix
 # Stop the service if running
 echo -e "${YELLOW}> Stopping voice server...${NC}"
 if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then
-    launchctl unload "$PLIST_PATH" 2>/dev/null
-    echo -e "${GREEN}OK Voice server stopped${NC}"
+    if launchctl unload "$PLIST_PATH" 2>/dev/null; then
+        echo -e "${GREEN}OK Voice server stopped${NC}"
+    else
+        echo -e "${RED}X Failed to stop voice server${NC}"
+        exit 1
+    fi
 else
     echo -e "${YELLOW}  Service was not running${NC}"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/uninstall.sh around lines 36 - 43, The uninstall
script currently calls launchctl unload "$PLIST_PATH" and then always prints "OK
Voice server stopped"; change it to check the exit status of launchctl unload
(or test with launchctl list after unload) and only print the success message
when unload succeeds; if unload fails, print an error (including SERVICE_NAME
and PLIST_PATH) and stop or fail the uninstall flow accordingly so the script
does not claim "Uninstall Complete" while the LaunchAgent remains loaded; locate
this logic around the block referencing SERVICE_NAME, PLIST_PATH and launchctl
unload in uninstall.sh.
.opencode/PAI/doc-dependencies.json-98-132 (1)

98-132: ⚠️ Potential issue | 🟠 Major

Track THEPLUGINSYSTEM.md in the dependency manifest.

This PR adds .opencode/PAI/THEPLUGINSYSTEM.md, but authoritative_docs still only enumerates THEHOOKSYSTEM.md. Any integrity tooling driven by this file will miss the new plugin-system doc, despite DOCUMENTATIONINDEX.md being marked tracks_all.

📝 Proposed fix
     "THEHOOKSYSTEM.md": {
       "description": "Hook system detailed documentation",
       "tier": 2,
       "upstream": "PAISYSTEMARCHITECTURE.md"
     },
+
+    "THEPLUGINSYSTEM.md": {
+      "description": "Plugin system detailed documentation",
+      "tier": 2,
+      "upstream": "PAISYSTEMARCHITECTURE.md"
+    },
 
     "PAIAGENTSYSTEM.md": {
       "description": "Agent system detailed documentation",
       "tier": 2,

If PAISYSTEMARCHITECTURE.md now has a dedicated plugin-system section as part of the Hooks → Plugins ADR, wire that dependent relationship in here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/doc-dependencies.json around lines 98 - 132, The dependency
manifest is missing an entry for THEPLUGINSYSTEM.md so integrity tooling will
ignore the new doc; add a new object keyed "THEPLUGINSYSTEM.md" in
.opencode/PAI/doc-dependencies.json with a short "description" (e.g., "Plugin
system documentation"), "tier": 2, and set "upstream":
"PAISYSTEMARCHITECTURE.md" to reflect the Hooks → Plugins relationship (or omit
upstream only if PAISYSTEMARCHITECTURE.md does not contain the plugin section);
ensure this mirrors the style of existing entries like THEHOOKSYSTEM.md and that
DOCUMENTATIONINDEX.md's tracks_all remains unchanged.
.opencode/VoiceServer/restart.sh-15-23 (1)

15-23: ⚠️ Potential issue | 🟠 Major

Propagate stop/start failures before reporting success.

This always reaches the restarted message, so a failing stop.sh or start.sh is converted into a false-positive restart. .opencode/VoiceServer/stop.sh:21-25 already exits non-zero on failure, but that signal is ignored here.

🐛 Proposed fix
 # Stop the server
-"$SCRIPT_DIR/stop.sh"
+if ! "$SCRIPT_DIR/stop.sh"; then
+    exit 1
+fi
 
 # Wait a moment
 sleep 2
 
 # Start the server
-"$SCRIPT_DIR/start.sh"
+if ! "$SCRIPT_DIR/start.sh"; then
+    exit 1
+fi
 
 echo -e "${GREEN}OK Voice server restarted${NC}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/restart.sh around lines 15 - 23, The restart script
currently ignores failures from "$SCRIPT_DIR/stop.sh" and "$SCRIPT_DIR/start.sh"
and always prints success; update restart.sh to propagate failures by checking
and acting on each command's exit status (or enable strict failure handling),
e.g., run "$SCRIPT_DIR/stop.sh" and if it exits non‑zero immediately exit with
that code, only proceed to sleep and "$SCRIPT_DIR/start.sh" when stop succeeds,
and likewise exit with start.sh's exit code on failure before printing the final
"OK Voice server restarted" message; reference the existing
"$SCRIPT_DIR/stop.sh", "$SCRIPT_DIR/start.sh", and the echo at the end to locate
where to add the checks.
.opencode/VoiceServer/menubar/install-menubar.sh-62-69 (1)

62-69: ⚠️ Potential issue | 🟠 Major

The installed plugin still points menu actions at the default ~/.claude tree.

pai-voice.5s.sh builds its Start/Stop/Restart actions from VOICE_SERVER_DIR, but .opencode/VoiceServer/menubar/pai-voice.5s.sh:7-8 defaults PAI_DIR to $HOME/.claude and .opencode/VoiceServer/menubar/pai-voice.5s.sh:31-49 expands that into .../VoiceServer/*.sh commands. Because .opencode/VoiceServer/install.sh:198-205 calls this installer without exporting an override, the raw symlink here makes the menubar actions target the wrong directory.

🐛 Proposed fix
 # Remove existing plugin if it exists
 rm -f "$PLUGIN_DIR/pai-voice.5s.sh" 2>/dev/null || true
 
-# Create symbolic link to our script
-ln -s "$MENUBAR_SCRIPT" "$PLUGIN_DIR/pai-voice.5s.sh"
+# Install a wrapper so the menubar process keeps the real PAI root
+PAI_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
+cat > "$PLUGIN_DIR/pai-voice.5s.sh" <<EOF
+#!/bin/bash
+export PAI_DIR="$PAI_DIR"
+exec "$MENUBAR_SCRIPT"
+EOF
+chmod +x "$PLUGIN_DIR/pai-voice.5s.sh"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/menubar/install-menubar.sh around lines 62 - 69, The
installer currently creates a raw symlink so the menubar plugin
(pai-voice.5s.sh) picks up its default PAI_DIR ($HOME/.claude) instead of the
real VOICE_SERVER_DIR; instead of ln -s to PLUGIN_DIR, copy MENUBAR_SCRIPT into
PLUGIN_DIR/pai-voice.5s.sh and inject or prepend an explicit export/assignment
of PAI_DIR="$VOICE_SERVER_DIR" (or otherwise ensure VOICE_SERVER_DIR is
exported) so the plugin builds Start/Stop/Restart from the correct path; ensure
you still rm -f the old plugin, make the copied file executable, and keep
references to MENUBAR_SCRIPT, PLUGIN_DIR, pai-voice.5s.sh and VOICE_SERVER_DIR
to locate the changes.
.opencode/PAI/THEHOOKSYSTEM.md-374-403 (1)

374-403: ⚠️ Potential issue | 🟠 Major

The voice configuration examples weren't updated for default routing.

This PR moves notifications to voice_id: "default" with provider-side resolution, but these sections still teach an explicit voiceId/ElevenLabs-ID setup and troubleshoot invalid hardcoded IDs. That sends readers back to the deprecated configuration model instead of the new default-routing flow.

Also applies to: 461-485, 824-843, 1150-1154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEHOOKSYSTEM.md around lines 374 - 403, Update the voice
configuration examples and explanatory text to use the new default-routing model
instead of hardcoded ElevenLabs IDs: replace references showing a direct
ElevenLabs voiceId (e.g., the JSON daidentity.voiceId example and any usage
showing getVoiceId() returning an ElevenLabs-ID) with guidance to set voice_id:
"default" and note that provider-side resolution will pick the actual voice;
update associated examples and troubleshooting that mention invalid hardcoded
IDs (affecting the sections around the getVoiceId/getIdentity usage and the
ranges called out) so they instruct readers to rely on "default" routing and
provider configuration rather than embedding an ElevenLabs voiceId.
.opencode/PAI/PIPELINES.md-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major

The documented pipeline location is inconsistent.

Line 9 says personal definitions live under USER/PIPELINES/, but Line 86 and Lines 161-165 tell users to create them under ~/.opencode/PAI/PIPELINES/. Only one location can be correct, so this guide currently sends readers to different trees depending on which section they follow.

Also applies to: 86-86, 161-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/PIPELINES.md at line 9, The doc contains two inconsistent
paths for personal pipeline definitions: the string "USER/PIPELINES/" (appearing
in the header/line 9) and the string "~/.opencode/PAI/PIPELINES/" (appearing
around lines 86 and 161-165); pick the correct canonical location and update all
occurrences so they match (replace every "USER/PIPELINES/" or
"~/.opencode/PAI/PIPELINES/" instance with the chosen path), and ensure any
examples or instructions that reference creating directories use that same
canonical path throughout the file.
.opencode/PAI/Tools/ValidateSkillStructure.ts-51-64 (1)

51-64: ⚠️ Potential issue | 🟠 Major

visitedPaths is never populated before recursion.

The cycle check at Lines 63-64 always sees the original empty set because no canonical path is added before Line 141 recurses. A symlink loop will still recurse indefinitely despite the guard.

Also applies to: 140-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/ValidateSkillStructure.ts around lines 51 - 64, The
cycle-guard Set visitedPaths is never populated before recursion in
scanDirectory, so symlinked directory cycles still recurse; fix by adding the
resolved canonical path (the value from realpath assigned to canonical) to
visitedPaths before recursing into scanDirectory (and optionally remove it after
returning if you want per-branch tracking), and ensure the same pattern is
applied at the other recursion site around lines that call scanDirectory
(references: function scanDirectory, variables visitedPaths and canonical, and
the recursive scanDirectory(...) calls).
.opencode/PAISECURITYSYSTEM/HOOKS.md-60-71 (1)

60-71: ⚠️ Potential issue | 🟠 Major

The YAML pattern-loading section is not backed by the implementation.

Lines 62-66 and 244-254 say the validator loads patterns.yaml and patterns.example.yaml, but the handler currently uses in-code DANGEROUS_PATTERNS and WARNING_PATTERNS, and patterns.example.yaml is not consumed. Leaving this here suggests a customization path that has no effect.

Also applies to: 75-117, 242-254

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAISECURITYSYSTEM/HOOKS.md around lines 60 - 71, The docs describe
cascading pattern loading but the code uses hard-coded DANGEROUS_PATTERNS and
WARNING_PATTERNS and never reads patterns.example.yaml; update the handler to
load patterns from USER/PAISECURITYSYSTEM/patterns.yaml first, fall back to
PAISECURITYSYSTEM/patterns.example.yaml if the user file is missing, and
fail-open (allow all) only if neither file exists. Concretely, add a YAML load
routine (using your existing YAML parser) invoked during handler initialization
(the function/method that currently references DANGEROUS_PATTERNS and
WARNING_PATTERNS) to populate those pattern variables from the loaded file, keep
the in-code arrays only as last-resort defaults or remove them, and ensure the
code path that checks patterns references the loaded pattern structures so
patterns.example.yaml is actually consumed.
.opencode/VoiceServer/start.sh-24-29 (1)

24-29: ⚠️ Potential issue | 🟠 Major

Don't return success before /health is actually up.

Line 25 short-circuits on the LaunchAgent label, but the real readiness signal in this file is the /health probe at Lines 39-45. That means this script can exit 0 without ever proving the server is reachable, and Lines 43-46 also keep a failed startup as success. Make the health check the success condition in both branches and fail if it never comes up.

Also applies to: 38-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/start.sh around lines 24 - 29, The current early
return when the LaunchAgent label ($SERVICE_NAME) is present lets the script
succeed before the actual readiness probe (/health) is verified; change both the
"already running" branch and the post-start branch to actively poll the /health
endpoint (with a short sleep/retry loop and a timeout) and only exit 0 once
/health returns success, otherwise exit non-zero after the timeout; update the
checks around launchctl list/launchctl start and the health probe logic at the
existing /health polling block so both branches reuse the same polling routine
and fail if the probe never becomes healthy.
.opencode/VoiceServer/menubar/pai-voice.5s.sh-7-8 (1)

7-8: ⚠️ Potential issue | 🟠 Major

The fallback path still points at the pre-rename install root.

Lines 7-8 default VOICE_SERVER_DIR to ~/.claude/VoiceServer, but this PR moved the service to .opencode/VoiceServer. If SwiftBar doesn't inherit PAI_DIR, Start/Stop/Restart/Status will all target scripts that are no longer there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/menubar/pai-voice.5s.sh around lines 7 - 8, The
fallback PAI_DIR/VOICE_SERVER_DIR values still point to the old install root;
update the defaults so PAI_DIR falls back to "$HOME/.opencode" (or set
VOICE_SERVER_DIR to "$HOME/.opencode/VoiceServer" when PAI_DIR is unset) so
Start/Stop/Restart/Status target the moved service; modify the PAI_DIR and/or
VOICE_SERVER_DIR assignments (symbols: PAI_DIR and VOICE_SERVER_DIR) to use the
new .opencode location.
.opencode/PAI/Tools/GenerateSkillIndex.ts-52-87 (1)

52-87: ⚠️ Potential issue | 🟠 Major

Symlinked directory walks need cycle detection here too.

Lines 63-87 follow directory symlinks recursively, but this walker never records canonical paths. A loop or duplicate symlink under skills/ can re-enter the same tree forever and hang index generation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/GenerateSkillIndex.ts around lines 52 - 87,
findSkillFiles can infinite-loop when following directory symlinks because it
never tracks canonical paths; modify findSkillFiles to accept or create a
visited Set<string> of realpaths and, before recursing into fullPath (and before
processing the directory), call fs.promises.realpath(fullPath) to get its
canonical path, skip if that canonical path is already in visited, otherwise add
it to visited and then recurse using the same visited set; reference symbols:
findSkillFiles, fullPath, entry.isSymbolicLink, stat, and the recursive call
findSkillFiles(fullPath) so you replace that recursion with a checked call that
passes the visited set to prevent cycles and duplicate traversal.
.opencode/PAI/Tools/GenerateSkillIndex.ts-374-374 (1)

374-374: ⚠️ Potential issue | 🟠 Major

Unexpected generator failures still report success.

Line 374 only prints the rejection. If scanning or writeFile() fails, automation will keep the stale skill-index.json because the process still exits 0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/GenerateSkillIndex.ts at line 374, The current top-level
call main().catch(console.error) only logs rejections and leaves the process
exit code as 0, so failures in main (including writeFile() errors when
generating skill-index.json) appear successful; change the catch to log the
error and terminate the process with a non-zero exit (e.g., in the main().catch
handler call process.exit(1) after console.error) so any thrown errors from main
or writeFile() cause CI/automation to fail and prevent stale skill-index.json
from being considered a success.
.opencode/PAI/Tools/ValidateSkillStructure.ts-97-105 (1)

97-105: ⚠️ Potential issue | 🟠 Major

Category metadata is being counted as a flat skill.

Once a top-level category adds the Category/SKILL.md required by Lines 112-120, the pathParts.length === 1 branch at Lines 102-105 increments flatSkills and validates that category as a normal skill. That makes the flat/hierarchical totals unreliable for any categorized tree.

Also applies to: 112-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/ValidateSkillStructure.ts around lines 97 - 105, The
flat-skill branch is counting top-level category metadata (the Category/SKILL.md
files) as regular skills; update the logic around pathParts, flatSkills and the
validateSkill call to skip category metadata files — e.g., detect and ignore
files whose basename is "SKILL.md" or whose first path part equals "Category"
(or otherwise matches your category metadata pattern) before incrementing
flatSkills and calling validateSkill so category metadata isn't included in
flat/hierarchical totals.
.opencode/PAI/Tools/SkillSearch.ts-203-203 (1)

203-203: ⚠️ Potential issue | 🟠 Major

Unexpected failures still exit 0.

Line 203 only logs the rejection. If readFile() or JSON.parse() fails, wrappers and CI jobs will still see a successful run.

Suggested fix
-main().catch(console.error);
+main().catch((error) => {
+  console.error(error);
+  process.exit(1);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/SkillSearch.ts at line 203, The top-level promise
rejection handler currently only logs errors (main().catch(console.error)), so
unexpected failures (e.g., readFile()/JSON.parse() inside main) still return
exit code 0; change the rejection handler to log the error and exit non‑zero
(for example, replace the simple console.error handler with a callback that
calls console.error(err) and then process.exit(1)) so the process reflects
failure when main() rejects.
.opencode/PAISECURITYSYSTEM/HOOKS.md-9-17 (1)

9-17: ⚠️ Potential issue | 🟠 Major

This doc describes a hook entrypoint that the codebase no longer exposes.

The current implementation lives in .opencode/plugins/handlers/security-validator.ts and is wired through .opencode/plugins/pai-unified.ts, so the stdin/exit-code contract here, the settings.json registration in Lines 160-191, and the direct bun ~/.opencode/hooks/SecurityValidator.hook.ts tests in Lines 221-235 all point to the wrong integration surface.

Also applies to: 48-57, 156-191, 219-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAISECURITYSYSTEM/HOOKS.md around lines 9 - 17, The HOOKS.md entry
for SecurityValidator.hook.ts is stale: update the doc to reference the actual
implementation at .opencode/plugins/handlers/security-validator.ts and its
registration/wiring via .opencode/plugins/pai-unified.ts instead of the old
stdin/exit-code contract, remove or correct the incorrect settings.json
registration and direct bun hook test instructions, and make sure the
Trigger/Matchers section and any example invocation reflect how
.opencode/plugins/pai-unified.ts wires the hook into PreToolUse so readers
follow the real integration surface.
.opencode/PAI/THEHOOKSYSTEM.md-354-368 (1)

354-368: ⚠️ Potential issue | 🟠 Major

The copied PAI_DIR example still points to the old install root.

This file consistently documents ~/.opencode, but Lines 357-362 tell users to set PAI_DIR="$HOME/.claude". If they paste that block, every ${PAI_DIR}/hooks/... example resolves to the wrong directory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEHOOKSYSTEM.md around lines 354 - 368, The PAI_DIR example
currently points to the old install root ("$HOME/.claude"); update the JSON
snippet so PAI_DIR matches the rest of the document (e.g., "PAI_DIR":
"$HOME/.opencode" or "~/.opencode") so that all ${PAI_DIR}/hooks/... paths
resolve correctly; change the example value for the PAI_DIR environment variable
in the block that shows the env section to the new install root used throughout
the file.
.opencode/VoiceServer/menubar/pai-voice.5s.sh-44-48 (1)

44-48: ⚠️ Potential issue | 🟠 Major

SwiftBar bash= parameter must reference an executable path, not inline command strings.

Lines 44, 46, and 48 embed full shell commands as string values in bash=. SwiftBar's bash= parameter accepts only an absolute file path to an executable; parameters are passed separately via param0=, param1=, etc. The correct pattern is bash=/bin/bash param0=-c param1="your command here".

To fix:

  • Line 44 (tail): bash=/bin/bash param0=-c param1="tail -f ~/Library/Logs/pai-voice-server.log"
  • Line 46 (curl): bash=/bin/bash param0=-c param1='curl -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d "{\"message\":\"Testing voice server\"}"'
  • Line 48 (open): bash=/usr/bin/open param1="$VOICE_SERVER_DIR"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/menubar/pai-voice.5s.sh around lines 44 - 48, Replace
the SwiftBar menu item lines that use inline commands inside bash= (the three
echo lines containing "tail -f ~/Library/Logs/pai-voice-server.log", the curl
POST command, and "open $VOICE_SERVER_DIR") with the proper SwiftBar invocation
form: set bash to an absolute executable path (e.g., bash=/bin/bash for tail and
curl, bash=/usr/bin/open for open), pass -c via param0 when using /bin/bash, and
put the full command in param1 (ensuring proper quoting/escaping for the curl
JSON payload); for the open action use bash=/usr/bin/open and pass the directory
via param1="$VOICE_SERVER_DIR".
.opencode/VoiceServer/install.sh-136-149 (1)

136-149: ⚠️ Potential issue | 🟠 Major

Harden startup verification to avoid flaky installs and false-positive smoke tests.

Line 137 uses a fixed delay plus a single health check, and Lines 146-149 do not fail on HTTP errors from /notify. This can mark installs as failed/successful incorrectly.

Suggested reliability fix
-# Wait for server to start
-sleep 2
-
-# Test the server
-echo -e "${YELLOW}> Testing voice server...${NC}"
-if curl -s -f -X GET http://localhost:8888/health > /dev/null 2>&1; then
+echo -e "${YELLOW}> Testing voice server...${NC}"
+max_attempts=15
+attempt=1
+until curl -s -f http://localhost:8888/health > /dev/null 2>&1; do
+    if [ "$attempt" -ge "$max_attempts" ]; then
+        echo -e "${RED}X Voice server is not responding${NC}"
+        echo "  Check logs at: $LOG_PATH"
+        echo "  Try running manually: bun run $SCRIPT_DIR/server.ts"
+        exit 1
+    fi
+    sleep 1
+    attempt=$((attempt + 1))
+done
+
+if curl -s -f http://localhost:8888/health > /dev/null 2>&1; then
     echo -e "${GREEN}OK Voice server is running${NC}"
 
     # Send test notification
     echo -e "${YELLOW}> Sending test notification...${NC}"
-    curl -s -X POST http://localhost:8888/notify \
+    curl -s -f -X POST http://localhost:8888/notify \
         -H "Content-Type: application/json" \
         -d '{"message": "Voice server installed successfully"}' > /dev/null
     echo -e "${GREEN}OK Test notification sent${NC}"
 else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/install.sh around lines 136 - 149, The startup
verification is flaky because it uses a fixed sleep then a single curl health
check and ignores HTTP errors on /notify; replace the fixed sleep + one-shot
curl against /health with a retry loop (e.g., attempt curl -f to
http://localhost:8888/health with a short delay between tries and an overall
timeout) so the script only proceeds when the service actually responds 200, and
also make the POST to /notify use a failing curl flag and check its exit status
(fail the install with a non-zero exit if the notify request fails); update the
health check and notify invocations in the install.sh snippet (the curl calls to
/health and /notify) accordingly.
🟡 Minor comments (3)
.opencode/VoiceServer/pronunciations.json-1-6 (1)

1-6: ⚠️ Potential issue | 🟡 Minor

Clarify or update the source of truth reference.

The _comment references skills/PAI/USER/PRONUNCIATIONS.md as the source of truth, but this file does not exist in the repository. If a source documentation file is intended, ensure it exists at the correct path. If planning to create it, use .opencode/skills/PAI/USER/PRONUNCIATIONS.md for consistency with the project's directory structure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/pronunciations.json around lines 1 - 6, Update the
_comment entry in pronunciations.json to point to the correct source-of-truth
path or create the referenced file: either change "_comment" to reference
".opencode/skills/PAI/USER/PRONUNCIATIONS.md" or add that file under
.opencode/skills/PAI/USER with the documentation for the entries in the
"replacements" array (e.g., PAI and ISC); ensure the string in "_comment"
matches the actual file location so the repository reference is accurate.
.opencode/VoiceServer/status.sh-59-75 (1)

59-75: ⚠️ Potential issue | 🟡 Minor

Status output still assumes ElevenLabs-only routing.

The PR switches voice resolution to TTS_PROVIDER plus dynamic "default" voices, but this block still keys entirely off ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID and otherwise reports macOS say. Any non-ElevenLabs provider will now look like fallback or misconfiguration even when it is working.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/status.sh around lines 59 - 75, Update the status
block to prefer TTS_PROVIDER (and the chosen voice variable, e.g., TTS_VOICE or
dynamic "default") rather than only checking ELEVENLABS_API_KEY/VOICE_ID: read
TTS_PROVIDER from ENV_FILE (falling back to checking ELEVENLABS_API_KEY for
compatibility), print "Provider: $TTS_PROVIDER" and the resolved voice id/name
(e.g., TTS_VOICE or ELEVENLABS_VOICE_ID when provider is elevenlabs), and only
show the macOS 'say' fallback when no provider is configured; adjust the
conditional logic around ENV_FILE, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID,
TTS_PROVIDER and TTS_VOICE to reflect this.
.opencode/PAI/THEPLUGINSYSTEM.md-248-260 (1)

248-260: ⚠️ Potential issue | 🟡 Minor

The library count is inconsistent.

This section says 8 libraries, but the table here lists 9, and the header / architecture sections also say 9.

📝 Suggested fix
-## Library Reference (8 Libraries)
+## Library Reference (9 Libraries)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEPLUGINSYSTEM.md around lines 248 - 260, Update the
mismatched library count by changing the header "Library Reference (8
Libraries)" to the correct number "Library Reference (9 Libraries)" and verify
any other occurrences of the count in this document (e.g., architecture/summary
sections) to match; confirm the table entries (file-logger.ts, paths.ts,
identity.ts, time.ts, sanitizer.ts, injection-patterns.ts, learning-utils.ts,
model-config.ts, db-utils.ts) are all intended and if not remove the extra row
instead of changing the header. Ensure the header text and any summary mentions
use the same numeric value so the document is consistent.
🧹 Nitpick comments (1)
.opencode/VoiceServer/install.sh (1)

54-56: Make ELEVENLABS_API_KEY parsing more robust.

Lines 54-56 can misread quoted values, whitespace, or multiple entries. Prefer extracting the last exact-key match and trimming quotes.

Suggested parsing refactor
-if [ -f "$ENV_FILE" ] && grep -q "ELEVENLABS_API_KEY=" "$ENV_FILE"; then
-    API_KEY=$(grep "ELEVENLABS_API_KEY=" "$ENV_FILE" | cut -d'=' -f2)
+if [ -f "$ENV_FILE" ] && grep -qE '^[[:space:]]*ELEVENLABS_API_KEY=' "$ENV_FILE"; then
+    API_KEY=$(
+      grep -E '^[[:space:]]*ELEVENLABS_API_KEY=' "$ENV_FILE" \
+      | tail -n 1 \
+      | sed -E 's/^[[:space:]]*ELEVENLABS_API_KEY=[[:space:]]*//; s/^[\"\x27]//; s/[\"\x27]$//'
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/VoiceServer/install.sh around lines 54 - 56, The current parsing
of ELEVENLABS_API_KEY in ENV_FILE can mis-handle quoted values, whitespace, or
multiple occurrences; update the logic that sets API_KEY to: search only for
lines that begin with the exact key (use a caret-anchored match for
ELEVENLABS_API_KEY=), take the last matching line if multiple exist, split on
the first '=' to get the full RHS, then trim leading/trailing whitespace and any
surrounding single or double quotes before assigning to API_KEY and performing
the existing checks (API_KEY != "your_api_key_here" and -n check). Ensure these
steps are applied where ENV_FILE and API_KEY are referenced so quoted or spaced
values and duplicate keys are handled robustly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/PAI/Tools/SkillSearch.ts:
- Line 22: The INDEX_FILE constant currently points to '..', 'Skills',
'skill-index.json' but the generator writes the index to
'.opencode/skills/skill-index.json'; update the INDEX_FILE definition (the join
call that constructs the path for INDEX_FILE) to point to the generator's
location (use 'skills' lowercase and the same relative path used by
GenerateSkillIndex.ts) so the code that reads the index can find the generated
file.

In @.opencode/PAI/Tools/ValidateSkillStructure.ts:
- Line 22: The SKILLS_DIR constant in ValidateSkillStructure.ts is pointing
three parents up (join(import.meta.dir, '..', '..', '..', 'skills')) which
causes the validator to scan the wrong skills tree; update SKILLS_DIR to resolve
two parents up to match the generator (i.e., use join(import.meta.dir, '..',
'..', 'skills')) so the validator and GenerateSkillIndex.ts reference the same
skills/ directory (confirm by comparing with the path used in
GenerateSkillIndex.ts and adjust any related imports or tests).

---

Major comments:
In @.opencode/PAI/doc-dependencies.json:
- Around line 98-132: The dependency manifest is missing an entry for
THEPLUGINSYSTEM.md so integrity tooling will ignore the new doc; add a new
object keyed "THEPLUGINSYSTEM.md" in .opencode/PAI/doc-dependencies.json with a
short "description" (e.g., "Plugin system documentation"), "tier": 2, and set
"upstream": "PAISYSTEMARCHITECTURE.md" to reflect the Hooks → Plugins
relationship (or omit upstream only if PAISYSTEMARCHITECTURE.md does not contain
the plugin section); ensure this mirrors the style of existing entries like
THEHOOKSYSTEM.md and that DOCUMENTATIONINDEX.md's tracks_all remains unchanged.

In @.opencode/PAI/PIPELINES.md:
- Line 9: The doc contains two inconsistent paths for personal pipeline
definitions: the string "USER/PIPELINES/" (appearing in the header/line 9) and
the string "~/.opencode/PAI/PIPELINES/" (appearing around lines 86 and 161-165);
pick the correct canonical location and update all occurrences so they match
(replace every "USER/PIPELINES/" or "~/.opencode/PAI/PIPELINES/" instance with
the chosen path), and ensure any examples or instructions that reference
creating directories use that same canonical path throughout the file.

In @.opencode/PAI/THEHOOKSYSTEM.md:
- Around line 374-403: Update the voice configuration examples and explanatory
text to use the new default-routing model instead of hardcoded ElevenLabs IDs:
replace references showing a direct ElevenLabs voiceId (e.g., the JSON
daidentity.voiceId example and any usage showing getVoiceId() returning an
ElevenLabs-ID) with guidance to set voice_id: "default" and note that
provider-side resolution will pick the actual voice; update associated examples
and troubleshooting that mention invalid hardcoded IDs (affecting the sections
around the getVoiceId/getIdentity usage and the ranges called out) so they
instruct readers to rely on "default" routing and provider configuration rather
than embedding an ElevenLabs voiceId.
- Around line 354-368: The PAI_DIR example currently points to the old install
root ("$HOME/.claude"); update the JSON snippet so PAI_DIR matches the rest of
the document (e.g., "PAI_DIR": "$HOME/.opencode" or "~/.opencode") so that all
${PAI_DIR}/hooks/... paths resolve correctly; change the example value for the
PAI_DIR environment variable in the block that shows the env section to the new
install root used throughout the file.

In @.opencode/PAI/Tools/GenerateSkillIndex.ts:
- Around line 52-87: findSkillFiles can infinite-loop when following directory
symlinks because it never tracks canonical paths; modify findSkillFiles to
accept or create a visited Set<string> of realpaths and, before recursing into
fullPath (and before processing the directory), call
fs.promises.realpath(fullPath) to get its canonical path, skip if that canonical
path is already in visited, otherwise add it to visited and then recurse using
the same visited set; reference symbols: findSkillFiles, fullPath,
entry.isSymbolicLink, stat, and the recursive call findSkillFiles(fullPath) so
you replace that recursion with a checked call that passes the visited set to
prevent cycles and duplicate traversal.
- Line 374: The current top-level call main().catch(console.error) only logs
rejections and leaves the process exit code as 0, so failures in main (including
writeFile() errors when generating skill-index.json) appear successful; change
the catch to log the error and terminate the process with a non-zero exit (e.g.,
in the main().catch handler call process.exit(1) after console.error) so any
thrown errors from main or writeFile() cause CI/automation to fail and prevent
stale skill-index.json from being considered a success.

In @.opencode/PAI/Tools/SkillSearch.ts:
- Line 203: The top-level promise rejection handler currently only logs errors
(main().catch(console.error)), so unexpected failures (e.g.,
readFile()/JSON.parse() inside main) still return exit code 0; change the
rejection handler to log the error and exit non‑zero (for example, replace the
simple console.error handler with a callback that calls console.error(err) and
then process.exit(1)) so the process reflects failure when main() rejects.

In @.opencode/PAI/Tools/ValidateSkillStructure.ts:
- Around line 51-64: The cycle-guard Set visitedPaths is never populated before
recursion in scanDirectory, so symlinked directory cycles still recurse; fix by
adding the resolved canonical path (the value from realpath assigned to
canonical) to visitedPaths before recursing into scanDirectory (and optionally
remove it after returning if you want per-branch tracking), and ensure the same
pattern is applied at the other recursion site around lines that call
scanDirectory (references: function scanDirectory, variables visitedPaths and
canonical, and the recursive scanDirectory(...) calls).
- Around line 97-105: The flat-skill branch is counting top-level category
metadata (the Category/SKILL.md files) as regular skills; update the logic
around pathParts, flatSkills and the validateSkill call to skip category
metadata files — e.g., detect and ignore files whose basename is "SKILL.md" or
whose first path part equals "Category" (or otherwise matches your category
metadata pattern) before incrementing flatSkills and calling validateSkill so
category metadata isn't included in flat/hierarchical totals.

In @.opencode/PAISECURITYSYSTEM/HOOKS.md:
- Around line 60-71: The docs describe cascading pattern loading but the code
uses hard-coded DANGEROUS_PATTERNS and WARNING_PATTERNS and never reads
patterns.example.yaml; update the handler to load patterns from
USER/PAISECURITYSYSTEM/patterns.yaml first, fall back to
PAISECURITYSYSTEM/patterns.example.yaml if the user file is missing, and
fail-open (allow all) only if neither file exists. Concretely, add a YAML load
routine (using your existing YAML parser) invoked during handler initialization
(the function/method that currently references DANGEROUS_PATTERNS and
WARNING_PATTERNS) to populate those pattern variables from the loaded file, keep
the in-code arrays only as last-resort defaults or remove them, and ensure the
code path that checks patterns references the loaded pattern structures so
patterns.example.yaml is actually consumed.
- Around line 9-17: The HOOKS.md entry for SecurityValidator.hook.ts is stale:
update the doc to reference the actual implementation at
.opencode/plugins/handlers/security-validator.ts and its registration/wiring via
.opencode/plugins/pai-unified.ts instead of the old stdin/exit-code contract,
remove or correct the incorrect settings.json registration and direct bun hook
test instructions, and make sure the Trigger/Matchers section and any example
invocation reflect how .opencode/plugins/pai-unified.ts wires the hook into
PreToolUse so readers follow the real integration surface.

In @.opencode/VoiceServer/install.sh:
- Around line 136-149: The startup verification is flaky because it uses a fixed
sleep then a single curl health check and ignores HTTP errors on /notify;
replace the fixed sleep + one-shot curl against /health with a retry loop (e.g.,
attempt curl -f to http://localhost:8888/health with a short delay between tries
and an overall timeout) so the script only proceeds when the service actually
responds 200, and also make the POST to /notify use a failing curl flag and
check its exit status (fail the install with a non-zero exit if the notify
request fails); update the health check and notify invocations in the install.sh
snippet (the curl calls to /health and /notify) accordingly.

In @.opencode/VoiceServer/menubar/install-menubar.sh:
- Around line 62-69: The installer currently creates a raw symlink so the
menubar plugin (pai-voice.5s.sh) picks up its default PAI_DIR ($HOME/.claude)
instead of the real VOICE_SERVER_DIR; instead of ln -s to PLUGIN_DIR, copy
MENUBAR_SCRIPT into PLUGIN_DIR/pai-voice.5s.sh and inject or prepend an explicit
export/assignment of PAI_DIR="$VOICE_SERVER_DIR" (or otherwise ensure
VOICE_SERVER_DIR is exported) so the plugin builds Start/Stop/Restart from the
correct path; ensure you still rm -f the old plugin, make the copied file
executable, and keep references to MENUBAR_SCRIPT, PLUGIN_DIR, pai-voice.5s.sh
and VOICE_SERVER_DIR to locate the changes.

In @.opencode/VoiceServer/menubar/pai-voice.5s.sh:
- Around line 7-8: The fallback PAI_DIR/VOICE_SERVER_DIR values still point to
the old install root; update the defaults so PAI_DIR falls back to
"$HOME/.opencode" (or set VOICE_SERVER_DIR to "$HOME/.opencode/VoiceServer" when
PAI_DIR is unset) so Start/Stop/Restart/Status target the moved service; modify
the PAI_DIR and/or VOICE_SERVER_DIR assignments (symbols: PAI_DIR and
VOICE_SERVER_DIR) to use the new .opencode location.
- Around line 44-48: Replace the SwiftBar menu item lines that use inline
commands inside bash= (the three echo lines containing "tail -f
~/Library/Logs/pai-voice-server.log", the curl POST command, and "open
$VOICE_SERVER_DIR") with the proper SwiftBar invocation form: set bash to an
absolute executable path (e.g., bash=/bin/bash for tail and curl,
bash=/usr/bin/open for open), pass -c via param0 when using /bin/bash, and put
the full command in param1 (ensuring proper quoting/escaping for the curl JSON
payload); for the open action use bash=/usr/bin/open and pass the directory via
param1="$VOICE_SERVER_DIR".

In @.opencode/VoiceServer/restart.sh:
- Around line 15-23: The restart script currently ignores failures from
"$SCRIPT_DIR/stop.sh" and "$SCRIPT_DIR/start.sh" and always prints success;
update restart.sh to propagate failures by checking and acting on each command's
exit status (or enable strict failure handling), e.g., run "$SCRIPT_DIR/stop.sh"
and if it exits non‑zero immediately exit with that code, only proceed to sleep
and "$SCRIPT_DIR/start.sh" when stop succeeds, and likewise exit with start.sh's
exit code on failure before printing the final "OK Voice server restarted"
message; reference the existing "$SCRIPT_DIR/stop.sh", "$SCRIPT_DIR/start.sh",
and the echo at the end to locate where to add the checks.

In @.opencode/VoiceServer/start.sh:
- Around line 24-29: The current early return when the LaunchAgent label
($SERVICE_NAME) is present lets the script succeed before the actual readiness
probe (/health) is verified; change both the "already running" branch and the
post-start branch to actively poll the /health endpoint (with a short
sleep/retry loop and a timeout) and only exit 0 once /health returns success,
otherwise exit non-zero after the timeout; update the checks around launchctl
list/launchctl start and the health probe logic at the existing /health polling
block so both branches reuse the same polling routine and fail if the probe
never becomes healthy.

In @.opencode/VoiceServer/stop.sh:
- Around line 31-35: The script currently SIGKILLs any process on :8888 which
can kill unrelated apps; change the cleanup to only stop the VoiceServer
LaunchAgent process by either (a) reading and using a stored PID file (e.g.,
create/use a pidfile written when starting the server) or (b) resolving PIDs
from lsof -ti :8888 and verifying each PID's command line via ps -p <pid> -o
args= contains a known identifier (e.g., the VoiceServer/LaunchAgent job label
or the server binary name) before sending a kill; apply the same guarded logic
to the duplicate block in uninstall.sh so only matching processes are
terminated.

In @.opencode/VoiceServer/uninstall.sh:
- Around line 36-43: The uninstall script currently calls launchctl unload
"$PLIST_PATH" and then always prints "OK Voice server stopped"; change it to
check the exit status of launchctl unload (or test with launchctl list after
unload) and only print the success message when unload succeeds; if unload
fails, print an error (including SERVICE_NAME and PLIST_PATH) and stop or fail
the uninstall flow accordingly so the script does not claim "Uninstall Complete"
while the LaunchAgent remains loaded; locate this logic around the block
referencing SERVICE_NAME, PLIST_PATH and launchctl unload in uninstall.sh.

---

Minor comments:
In @.opencode/PAI/THEPLUGINSYSTEM.md:
- Around line 248-260: Update the mismatched library count by changing the
header "Library Reference (8 Libraries)" to the correct number "Library
Reference (9 Libraries)" and verify any other occurrences of the count in this
document (e.g., architecture/summary sections) to match; confirm the table
entries (file-logger.ts, paths.ts, identity.ts, time.ts, sanitizer.ts,
injection-patterns.ts, learning-utils.ts, model-config.ts, db-utils.ts) are all
intended and if not remove the extra row instead of changing the header. Ensure
the header text and any summary mentions use the same numeric value so the
document is consistent.

In @.opencode/VoiceServer/pronunciations.json:
- Around line 1-6: Update the _comment entry in pronunciations.json to point to
the correct source-of-truth path or create the referenced file: either change
"_comment" to reference ".opencode/skills/PAI/USER/PRONUNCIATIONS.md" or add
that file under .opencode/skills/PAI/USER with the documentation for the entries
in the "replacements" array (e.g., PAI and ISC); ensure the string in "_comment"
matches the actual file location so the repository reference is accurate.

In @.opencode/VoiceServer/status.sh:
- Around line 59-75: Update the status block to prefer TTS_PROVIDER (and the
chosen voice variable, e.g., TTS_VOICE or dynamic "default") rather than only
checking ELEVENLABS_API_KEY/VOICE_ID: read TTS_PROVIDER from ENV_FILE (falling
back to checking ELEVENLABS_API_KEY for compatibility), print "Provider:
$TTS_PROVIDER" and the resolved voice id/name (e.g., TTS_VOICE or
ELEVENLABS_VOICE_ID when provider is elevenlabs), and only show the macOS 'say'
fallback when no provider is configured; adjust the conditional logic around
ENV_FILE, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, TTS_PROVIDER and TTS_VOICE to
reflect this.

---

Nitpick comments:
In @.opencode/VoiceServer/install.sh:
- Around line 54-56: The current parsing of ELEVENLABS_API_KEY in ENV_FILE can
mis-handle quoted values, whitespace, or multiple occurrences; update the logic
that sets API_KEY to: search only for lines that begin with the exact key (use a
caret-anchored match for ELEVENLABS_API_KEY=), take the last matching line if
multiple exist, split on the first '=' to get the full RHS, then trim
leading/trailing whitespace and any surrounding single or double quotes before
assigning to API_KEY and performing the existing checks (API_KEY !=
"your_api_key_here" and -n check). Ensure these steps are applied where ENV_FILE
and API_KEY are referenced so quoted or spaced values and duplicate keys are
handled robustly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5dcbd132-a341-406d-83b8-fb019056c3df

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea21c1 and 2bd381c.

📒 Files selected for processing (74)
  • .opencode/PAI/Algorithm/v3.7.0.md
  • .opencode/PAI/DOCUMENTATIONINDEX.md
  • .opencode/PAI/PIPELINES.md
  • .opencode/PAI/SKILL.md
  • .opencode/PAI/THEHOOKSYSTEM.md
  • .opencode/PAI/THEPLUGINSYSTEM.md
  • .opencode/PAI/Tools/GenerateSkillIndex.ts
  • .opencode/PAI/Tools/SkillSearch.ts
  • .opencode/PAI/Tools/ValidateSkillStructure.ts
  • .opencode/PAI/Tools/algorithm.ts
  • .opencode/PAI/doc-dependencies.json
  • .opencode/PAISECURITYSYSTEM/HOOKS.md
  • .opencode/VoiceServer/README.md
  • .opencode/VoiceServer/install.sh
  • .opencode/VoiceServer/logs/README.md
  • .opencode/VoiceServer/menubar/install-menubar.sh
  • .opencode/VoiceServer/menubar/pai-voice.5s.sh
  • .opencode/VoiceServer/pronunciations.json
  • .opencode/VoiceServer/restart.sh
  • .opencode/VoiceServer/server.ts
  • .opencode/VoiceServer/start.sh
  • .opencode/VoiceServer/status.sh
  • .opencode/VoiceServer/stop.sh
  • .opencode/VoiceServer/uninstall.sh
  • .opencode/VoiceServer/voices.json
  • .opencode/agents/Algorithm.md
  • .opencode/skills/Utilities/AudioEditor/SKILL.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Analyze.help.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Analyze.ts
  • .opencode/skills/Utilities/AudioEditor/Tools/Edit.help.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Edit.ts
  • .opencode/skills/Utilities/AudioEditor/Tools/Pipeline.help.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Pipeline.ts
  • .opencode/skills/Utilities/AudioEditor/Tools/Polish.help.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Polish.ts
  • .opencode/skills/Utilities/AudioEditor/Tools/Transcribe.help.md
  • .opencode/skills/Utilities/AudioEditor/Tools/Transcribe.ts
  • .opencode/skills/Utilities/AudioEditor/Workflows/Clean.md
  • .opencode/skills/Utilities/CodeReview/SKILL.md
  • .opencode/skills/Utilities/OpenCodeSystem/SKILL.md
  • .opencode/skills/Utilities/Sales/SKILL.md
  • .opencode/skills/Utilities/Sales/Workflows/CreateNarrative.md
  • .opencode/skills/Utilities/Sales/Workflows/CreateSalesPackage.md
  • .opencode/skills/Utilities/Sales/Workflows/CreateVisual.md
  • .opencode/skills/Utilities/System/SKILL.md
  • .opencode/skills/Utilities/System/Tools/CreateUpdate.ts
  • .opencode/skills/Utilities/System/Tools/SecretScan.ts
  • .opencode/skills/Utilities/System/Tools/UpdateIndex.ts
  • .opencode/skills/Utilities/System/Tools/UpdateSearch.ts
  • .opencode/skills/Utilities/System/Workflows/CrossRepoValidation.md
  • .opencode/skills/Utilities/System/Workflows/DocumentRecent.md
  • .opencode/skills/Utilities/System/Workflows/DocumentSession.md
  • .opencode/skills/Utilities/System/Workflows/GitPush.md
  • .opencode/skills/Utilities/System/Workflows/IntegrityCheck.md
  • .opencode/skills/Utilities/System/Workflows/PrivacyCheck.md
  • .opencode/skills/Utilities/System/Workflows/SecretScanning.md
  • .opencode/skills/Utilities/System/Workflows/WorkContextRecall.md
  • .opencode/skills/Utilities/WriteStory/AestheticProfiles.md
  • .opencode/skills/Utilities/WriteStory/AntiCliche.md
  • .opencode/skills/Utilities/WriteStory/Critics.md
  • .opencode/skills/Utilities/WriteStory/RhetoricalFigures.md
  • .opencode/skills/Utilities/WriteStory/SKILL.md
  • .opencode/skills/Utilities/WriteStory/StorrFramework.md
  • .opencode/skills/Utilities/WriteStory/StoryLayers.md
  • .opencode/skills/Utilities/WriteStory/StoryStructures.md
  • .opencode/skills/Utilities/WriteStory/Workflows/BuildBible.md
  • .opencode/skills/Utilities/WriteStory/Workflows/Explore.md
  • .opencode/skills/Utilities/WriteStory/Workflows/Interview.md
  • .opencode/skills/Utilities/WriteStory/Workflows/Revise.md
  • .opencode/skills/Utilities/WriteStory/Workflows/WriteChapter.md
  • .opencode/skills/skill-index.json
  • .prd/PRD-20260309-coderabbit-pr47-fixes.md
  • .prd/PRD-20260309-installer-refactor.md
  • docs/architecture/INSTALLER-REFACTOR-PLAN.md
💤 Files with no reviewable changes (3)
  • .prd/PRD-20260309-coderabbit-pr47-fixes.md
  • .prd/PRD-20260309-installer-refactor.md
  • docs/architecture/INSTALLER-REFACTOR-PLAN.md

Fixes verified against actual code:

Tools path resolution:
- SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/
- ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2
- Both now match GenerateSkillIndex.ts (.opencode/skills/)

Symlink cycle prevention:
- GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths
- ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set

Error handling:
- GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero

Documentation accuracy:
- doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry
- THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode
- THEPLUGINSYSTEM.md: library count 8 → 9 (matches table)
- pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path

Stats accuracy:
- ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills
- pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode

Skipped (verified not applicable):
- PIPELINES.md paths: upstream semantics, both paths correct in context
- VoiceServer shell script hardening: upstream 4.0.3 files, out of scope
- PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture
- THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
.opencode/PAI/Tools/GenerateSkillIndex.ts (1)

86-90: Consider using async stat for consistency.

The function uses async file operations throughout but switches to synchronous existsSync here. While not a bug, using access from fs/promises would maintain consistency.

♻️ Optional refactor for async consistency
+import { readdir, readFile, writeFile, stat, realpath, access } from 'fs/promises';
+import { constants } from 'fs';
 import { join, relative, sep } from 'path';
-import { existsSync } from 'fs';
         // Check for SKILL.md in this directory
         const skillMdPath = join(fullPath, 'SKILL.md');
-        if (existsSync(skillMdPath)) {
+        try {
+          await access(skillMdPath, constants.F_OK);
           skillFiles.push(skillMdPath);
+        } catch {
+          // SKILL.md doesn't exist in this directory
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/Tools/GenerateSkillIndex.ts around lines 86 - 90, The code
currently uses the synchronous existsSync to check SKILL.md (skillMdPath) which
breaks the async pattern; replace the existsSync check with an async fs.promises
access/stat call (e.g., await access(skillMdPath) or await stat(skillMdPath))
and only push to skillFiles when the promise resolves, handling failures with a
try/catch so the function remains fully async; also update imports to use
fs/promises or destructure access/stat from 'fs/promises' and adjust the
surrounding function to await this check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/PAI/THEHOOKSYSTEM.md:
- Around line 493-495: The markdown has fenced code blocks missing language
identifiers (MD040) for the blocks that contain the template line
"YYYY-MM-DD-HHMMSS_TYPE_description.md" and the "HOOK LIFECYCLE:" section;
update each opening fence from ``` to ```text (or another appropriate language
label) so the blocks become ```text ... ``` to satisfy the linter and keep the
content unchanged inside the fences; ensure the same change is applied to the
other block referenced (the one beginning with "HOOK LIFECYCLE:" around the
second range).
- Around line 381-485: Update the doc text and examples that currently instruct
hardcoding an ElevenLabs voice ID to instead show the new
default/provider-resolution flow: state that daidentity.voiceId may be "default"
(or a provider-specific id), that runtime provider is selected via the
TTS_PROVIDER env var or per-agent voice registry, and update the example calls
and the VoiceNotification pattern (references: getIdentity, getVoiceId,
identity.voiceId, handlers/VoiceNotification.ts) to use voice_id: "default" and
include a short note about provider resolution and where to configure
provider-specific IDs if needed; apply the same change to all referenced
sections (including the earlier identity examples and the VoiceNotification
payload) so examples no longer instruct hardcoding ElevenLabs IDs.
- Around line 360-368: Update the conflicting PAI_DIR defaults by choosing one
canonical value and making both occurrences consistent: replace the JSON example
value "$HOME/.opencode" and the descriptive text "typically `~/.claude`" so they
both state the same default (e.g., "$HOME/.opencode" or "~/.claude"); ensure the
documentation still notes that hook scripts reference ${PAI_DIR} in command
paths and that the chosen value is used throughout the section and examples
(search for the PAI_DIR example and the "typically" sentence to locate the
edits).

In @.opencode/PAI/Tools/GenerateSkillIndex.ts:
- Around line 358-368: The token-estimate math can divide by zero when
index.totalSkills is 0 because currentTokens = index.totalSkills * avgFullTokens
may be 0; update the calculation around currentTokens/newTokens/savings
(referencing avgFullTokens, avgMinimalTokens, currentTokens, newTokens, savings,
and index.totalSkills) to guard against division by zero—compute savings only if
currentTokens > 0 (otherwise set savings to "0.0" or "0%") and ensure the
console output uses that safe value.

---

Nitpick comments:
In @.opencode/PAI/Tools/GenerateSkillIndex.ts:
- Around line 86-90: The code currently uses the synchronous existsSync to check
SKILL.md (skillMdPath) which breaks the async pattern; replace the existsSync
check with an async fs.promises access/stat call (e.g., await
access(skillMdPath) or await stat(skillMdPath)) and only push to skillFiles when
the promise resolves, handling failures with a try/catch so the function remains
fully async; also update imports to use fs/promises or destructure access/stat
from 'fs/promises' and adjust the surrounding function to await this check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65ce1a29-c3ec-4c8c-9781-89c08b34e896

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd381c and 5655797.

📒 Files selected for processing (8)
  • .opencode/PAI/THEHOOKSYSTEM.md
  • .opencode/PAI/THEPLUGINSYSTEM.md
  • .opencode/PAI/Tools/GenerateSkillIndex.ts
  • .opencode/PAI/Tools/SkillSearch.ts
  • .opencode/PAI/Tools/ValidateSkillStructure.ts
  • .opencode/PAI/doc-dependencies.json
  • .opencode/VoiceServer/menubar/pai-voice.5s.sh
  • .opencode/VoiceServer/pronunciations.json
✅ Files skipped from review due to trivial changes (4)
  • .opencode/PAI/doc-dependencies.json
  • .opencode/PAI/THEPLUGINSYSTEM.md
  • .opencode/PAI/Tools/SkillSearch.ts
  • .opencode/VoiceServer/menubar/pai-voice.5s.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • .opencode/VoiceServer/pronunciations.json
  • .opencode/PAI/Tools/ValidateSkillStructure.ts

Comment on lines +360 to +368
"PAI_DIR": "$HOME/.opencode",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000"
}
}
```

**Key Variables:**
- `PAI_DIR` - PAI installation directory (typically `~/.claude`)
- Hook scripts reference `${PAI_DIR}` in command paths
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Conflicting PAI_DIR defaults in the same section

Line 360 documents PAI_DIR as $HOME/.opencode, but Line 367 says it is typically ~/.claude. This contradiction can lead to broken hook paths during setup. Please keep one canonical default across both lines.

Suggested doc fix
- - `PAI_DIR` - PAI installation directory (typically `~/.claude`)
+ - `PAI_DIR` - PAI installation directory (typically `~/.opencode`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEHOOKSYSTEM.md around lines 360 - 368, Update the
conflicting PAI_DIR defaults by choosing one canonical value and making both
occurrences consistent: replace the JSON example value "$HOME/.opencode" and the
descriptive text "typically `~/.claude`" so they both state the same default
(e.g., "$HOME/.opencode" or "~/.claude"); ensure the documentation still notes
that hook scripts reference ${PAI_DIR} in command paths and that the chosen
value is used throughout the section and examples (search for the PAI_DIR
example and the "typically" sentence to locate the edits).

Comment on lines +381 to +485
"voiceId": "{YourElevenLabsVoiceId}"
},
"principal": {
"name": "{YourName}",
"pronunciation": "{YourName}",
"timezone": "America/Los_Angeles"
}
}
```

**Using the Identity Module:**
```typescript
import { getIdentity, getPrincipal, getDAName, getPrincipalName, getVoiceId } from './lib/identity';

// Get full identity objects
const identity = getIdentity(); // { name, fullName, displayName, voiceId, color }
const principal = getPrincipal(); // { name, pronunciation, timezone }

// Convenience functions
const DA_NAME = getDAName(); // "PAI"
const USER_NAME = getPrincipalName(); // "{YourName}"
const VOICE_ID = getVoiceId(); // from settings.json daidentity.voiceId
```

**Why settings.json?**
- Programmatic access via `JSON.parse()` - no regex parsing markdown
- Central to the PAI install wizard
- Single source of truth for all configuration
- Tool-friendly: easy to read/write from any language

### Hook Configuration Structure

```json
{
"hooks": {
"HookEventName": [
{
"matcher": "pattern", // Optional: filter which tools/events trigger hook
"hooks": [
{
"type": "command",
"command": "${PAI_DIR}/hooks/my-hook.ts --arg value"
}
]
}
]
}
}
```

**Fields:**
- `HookEventName` - One of: SessionStart, SessionEnd, UserPromptSubmit, Stop, PreToolUse, PostToolUse, PreCompact
- `matcher` - Pattern to match (use `"*"` for all tools, or specific tool names)
- `type` - Always `"command"` (executes external script)
- `command` - Path to executable hook script (TypeScript/Python/Bash)

### Hook Input (stdin)
All hooks receive JSON data on stdin:

```typescript
{
session_id: string; // Unique session identifier
transcript_path: string; // Path to JSONL transcript
hook_event_name: string; // Event that triggered hook
prompt?: string; // User prompt (UserPromptSubmit only)
tool_name?: string; // Tool name (PreToolUse/PostToolUse)
tool_input?: any; // Tool parameters (PreToolUse)
tool_output?: any; // Tool result (PostToolUse)
// ... event-specific fields
}
```

---

## Common Patterns

### 1. Voice Notifications

**Pattern:** Extract completion message → Send to voice server

```typescript
// handlers/VoiceNotification.ts pattern
import { getIdentity } from './lib/identity';

const identity = getIdentity();
const completionMessage = extractCompletionMessage(lastMessage);

const payload = {
title: identity.name,
message: completionMessage,
voice_enabled: true,
voice_id: identity.voiceId // From settings.json
};

await fetch('http://localhost:8888/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
```

**Agent-Specific Voices:**
Configure voice IDs via `settings.json` daidentity section or environment variables.
Each agent can have a unique ElevenLabs voice configured. See the Agents skill for voice registry.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Voice configuration guidance appears stale vs dynamic provider routing

This section still instructs explicit ElevenLabs voice-ID configuration (e.g., Line 381, Line 472, Line 484, Line 828), but this PR’s objective states voice_id: "default" with provider resolution via TTS_PROVIDER. The current text can misconfigure new installs.

Suggested doc alignment update
-    "voiceId": "{YourElevenLabsVoiceId}"
+    "voiceId": "default"
-  voice_id: identity.voiceId  // From settings.json
+  voice_id: "default"  // Provider-specific voice resolved server-side via TTS_PROVIDER
-Each agent can have a unique ElevenLabs voice configured. See the Agents skill for voice registry.
+Voice resolution is provider-aware and handled by VoiceServer using `TTS_PROVIDER`.
+Use `"default"` unless overriding with a provider-specific mapping.

Also applies to: 828-843, 1151-1154

🧰 Tools
🪛 LanguageTool

[grammar] ~483-~483: Ensure spelling is correct
Context: ...Configure voice IDs via settings.json daidentity section or environment variables. Each ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEHOOKSYSTEM.md around lines 381 - 485, Update the doc text
and examples that currently instruct hardcoding an ElevenLabs voice ID to
instead show the new default/provider-resolution flow: state that
daidentity.voiceId may be "default" (or a provider-specific id), that runtime
provider is selected via the TTS_PROVIDER env var or per-agent voice registry,
and update the example calls and the VoiceNotification pattern (references:
getIdentity, getVoiceId, identity.voiceId, handlers/VoiceNotification.ts) to use
voice_id: "default" and include a short note about provider resolution and where
to configure provider-specific IDs if needed; apply the same change to all
referenced sections (including the earlier identity examples and the
VoiceNotification payload) so examples no longer instruct hardcoding ElevenLabs
IDs.

Comment on lines +493 to +495
```
YYYY-MM-DD-HHMMSS_TYPE_description.md
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced code blocks

The fenced blocks starting at Line 493 and Line 1078 are missing language tags (MD040). Add explicit languages to keep lint clean.

Suggested markdown fix
-```
+```text
 YYYY-MM-DD-HHMMSS_TYPE_description.md
-```
+```
-```
+```text
 HOOK LIFECYCLE:
 ...
-```
+```

Also applies to: 1078-1155

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 493-493: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/PAI/THEHOOKSYSTEM.md around lines 493 - 495, The markdown has
fenced code blocks missing language identifiers (MD040) for the blocks that
contain the template line "YYYY-MM-DD-HHMMSS_TYPE_description.md" and the "HOOK
LIFECYCLE:" section; update each opening fence from ``` to ```text (or another
appropriate language label) so the blocks become ```text ... ``` to satisfy the
linter and keep the content unchanged inside the fences; ensure the same change
is applied to the other block referenced (the one beginning with "HOOK
LIFECYCLE:" around the second range).

…tion (ADR-001)

- Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md)
- Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md)
- Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename
  hook-system section to plugin-system referencing THEPLUGINSYSTEM.md
- Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology,
  execution flow updated for plugin model (throw Error, not exit codes)
- Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree
- Fix GenerateSkillIndex.ts division by zero when totalSkills == 0
@Steffen025 Steffen025 merged commit bcc0e06 into dev Mar 20, 2026
3 checks passed
Steffen025 added a commit that referenced this pull request Mar 21, 2026
…ver/, PAI root docs, voice ID fix (#83)

* refactor: move orphan skills into Utilities/ category

Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity:
- AudioEditor → Utilities/AudioEditor (was missing from Utilities)
- CodeReview → Utilities/CodeReview (OpenCode-specific)
- OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific)
- Sales → Utilities/Sales (OpenCode-specific)
- System → Utilities/System (OpenCode-specific)
- WriteStory → Utilities/WriteStory (OpenCode-specific)

OpenCode-specific skills preserved in Utilities/ category.

* feat: add missing PAI/ root docs from 4.0.3

Added 3 files missing from PAI/ root:
- doc-dependencies.json (documentation dependency graph)
- PIPELINES.md (pipeline system documentation)
- THEHOOKSYSTEM.md (hooks/plugin system documentation)

All .claude/ references replaced with .opencode/.

* refactor: rename voice-server/ to VoiceServer/ + add missing files

Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3)

Added missing files from PAI 4.0.3:
- install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh
- voices.json, pronunciations.json
- menubar/ directory

All .claude/ references replaced with .opencode/ in copied files.

* fix: restore missing files + remove legacy content from skills/PAI/ deletion

Restored from git history (accidentally deleted with skills/PAI/):
- PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md)
- PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts
- PAI/PIPELINES.md, PAI/doc-dependencies.json
- PAISECURITYSYSTEM/HOOKS.md

Removed legacy files NOT in 4.0.3 upstream:
- PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md,
  SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts)
- PAI/UPDATES/ directory (not in 4.0.3)
- PAI/Workflows/ (11 legacy workflows not in 4.0.3 root)
- PAI/USER/ reset to 4.0.3 template (README.md placeholders only,
  removed 47 personal template files that don't belong in public repo)

Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md

Regenerated skill-index.json (52 skills, 7 categories)

PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations:
- THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001)
- MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C)

* chore: remove INSTALLER-REFACTOR-PLAN.md (internal planning doc)

* fix: replace all hardcoded ElevenLabs voice IDs with dynamic routing

VoiceServer sendNotification() now uses title (agent name) instead of
voiceId for voice resolution. This enables dynamic provider switching
via TTS_PROVIDER env var (google/elevenlabs/macos).

Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName'

Files affected:
- VoiceServer/server.ts (line 425: voiceId → safeTitle)
- PAI/SKILL.md (7 phase curls)
- PAI/Algorithm/v3.7.0.md (2 voice curls)
- agents/Algorithm.md (frontmatter + 3 references)
- PAI/Tools/algorithm.ts (VOICE_ID constant)

Zero hardcoded ElevenLabs IDs remain in the codebase.

* fix: address CodeRabbit review findings on PR #83

Fixes verified against actual code:

Tools path resolution:
- SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/
- ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2
- Both now match GenerateSkillIndex.ts (.opencode/skills/)

Symlink cycle prevention:
- GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths
- ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set

Error handling:
- GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero

Documentation accuracy:
- doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry
- THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode
- THEPLUGINSYSTEM.md: library count 8 → 9 (matches table)
- pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path

Stats accuracy:
- ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills
- pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode

Skipped (verified not applicable):
- PIPELINES.md paths: upstream semantics, both paths correct in context
- VoiceServer shell script hardening: upstream 4.0.3 files, out of scope
- PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture
- THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs

* fix: delete THEHOOKSYSTEM.md and HOOKS.md, complete hook→plugin migration (ADR-001)

- Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md)
- Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md)
- Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename
  hook-system section to plugin-system referencing THEPLUGINSYSTEM.md
- Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology,
  execution flow updated for plugin model (throw Error, not exit codes)
- Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree
- Fix GenerateSkillIndex.ts division by zero when totalSkills == 0
Steffen025 added a commit that referenced this pull request Mar 21, 2026
…ver/, PAI root docs, voice ID fix (#83)

* refactor: move orphan skills into Utilities/ category

Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity:
- AudioEditor → Utilities/AudioEditor (was missing from Utilities)
- CodeReview → Utilities/CodeReview (OpenCode-specific)
- OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific)
- Sales → Utilities/Sales (OpenCode-specific)
- System → Utilities/System (OpenCode-specific)
- WriteStory → Utilities/WriteStory (OpenCode-specific)

OpenCode-specific skills preserved in Utilities/ category.

* feat: add missing PAI/ root docs from 4.0.3

Added 3 files missing from PAI/ root:
- doc-dependencies.json (documentation dependency graph)
- PIPELINES.md (pipeline system documentation)
- THEHOOKSYSTEM.md (hooks/plugin system documentation)

All .claude/ references replaced with .opencode/.

* refactor: rename voice-server/ to VoiceServer/ + add missing files

Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3)

Added missing files from PAI 4.0.3:
- install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh
- voices.json, pronunciations.json
- menubar/ directory

All .claude/ references replaced with .opencode/ in copied files.

* fix: restore missing files + remove legacy content from skills/PAI/ deletion

Restored from git history (accidentally deleted with skills/PAI/):
- PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md)
- PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts
- PAI/PIPELINES.md, PAI/doc-dependencies.json
- PAISECURITYSYSTEM/HOOKS.md

Removed legacy files NOT in 4.0.3 upstream:
- PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md,
  SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts)
- PAI/UPDATES/ directory (not in 4.0.3)
- PAI/Workflows/ (11 legacy workflows not in 4.0.3 root)
- PAI/USER/ reset to 4.0.3 template (README.md placeholders only,
  removed 47 personal template files that don't belong in public repo)

Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md

Regenerated skill-index.json (52 skills, 7 categories)

PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations:
- THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001)
- MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C)

* chore: remove INSTALLER-REFACTOR-PLAN.md (internal planning doc)

* fix: replace all hardcoded ElevenLabs voice IDs with dynamic routing

VoiceServer sendNotification() now uses title (agent name) instead of
voiceId for voice resolution. This enables dynamic provider switching
via TTS_PROVIDER env var (google/elevenlabs/macos).

Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName'

Files affected:
- VoiceServer/server.ts (line 425: voiceId → safeTitle)
- PAI/SKILL.md (7 phase curls)
- PAI/Algorithm/v3.7.0.md (2 voice curls)
- agents/Algorithm.md (frontmatter + 3 references)
- PAI/Tools/algorithm.ts (VOICE_ID constant)

Zero hardcoded ElevenLabs IDs remain in the codebase.

* fix: address CodeRabbit review findings on PR #83

Fixes verified against actual code:

Tools path resolution:
- SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/
- ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2
- Both now match GenerateSkillIndex.ts (.opencode/skills/)

Symlink cycle prevention:
- GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths
- ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set

Error handling:
- GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero

Documentation accuracy:
- doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry
- THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode
- THEPLUGINSYSTEM.md: library count 8 → 9 (matches table)
- pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path

Stats accuracy:
- ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills
- pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode

Skipped (verified not applicable):
- PIPELINES.md paths: upstream semantics, both paths correct in context
- VoiceServer shell script hardening: upstream 4.0.3 files, out of scope
- PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture
- THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs

* fix: delete THEHOOKSYSTEM.md and HOOKS.md, complete hook→plugin migration (ADR-001)

- Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md)
- Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md)
- Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename
  hook-system section to plugin-system referencing THEPLUGINSYSTEM.md
- Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology,
  execution flow updated for plugin model (throw Error, not exit codes)
- Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree
- Fix GenerateSkillIndex.ts division by zero when totalSkills == 0
Steffen025 added a commit that referenced this pull request Mar 22, 2026
#83#85) (#87)

* refactor(v3.0 Part B/2): add correct structure — Utilities/, VoiceServer/, PAI root docs, voice ID fix (#83)

* refactor: move orphan skills into Utilities/ category

Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity:
- AudioEditor → Utilities/AudioEditor (was missing from Utilities)
- CodeReview → Utilities/CodeReview (OpenCode-specific)
- OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific)
- Sales → Utilities/Sales (OpenCode-specific)
- System → Utilities/System (OpenCode-specific)
- WriteStory → Utilities/WriteStory (OpenCode-specific)

OpenCode-specific skills preserved in Utilities/ category.

* feat: add missing PAI/ root docs from 4.0.3

Added 3 files missing from PAI/ root:
- doc-dependencies.json (documentation dependency graph)
- PIPELINES.md (pipeline system documentation)
- THEHOOKSYSTEM.md (hooks/plugin system documentation)

All .claude/ references replaced with .opencode/.

* refactor: rename voice-server/ to VoiceServer/ + add missing files

Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3)

Added missing files from PAI 4.0.3:
- install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh
- voices.json, pronunciations.json
- menubar/ directory

All .claude/ references replaced with .opencode/ in copied files.

* fix: restore missing files + remove legacy content from skills/PAI/ deletion

Restored from git history (accidentally deleted with skills/PAI/):
- PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md)
- PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts
- PAI/PIPELINES.md, PAI/doc-dependencies.json
- PAISECURITYSYSTEM/HOOKS.md

Removed legacy files NOT in 4.0.3 upstream:
- PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md,
  SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts)
- PAI/UPDATES/ directory (not in 4.0.3)
- PAI/Workflows/ (11 legacy workflows not in 4.0.3 root)
- PAI/USER/ reset to 4.0.3 template (README.md placeholders only,
  removed 47 personal template files that don't belong in public repo)

Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md

Regenerated skill-index.json (52 skills, 7 categories)

PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations:
- THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001)
- MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C)

* chore: remove INSTALLER-REFACTOR-PLAN.md (internal planning doc)

* fix: replace all hardcoded ElevenLabs voice IDs with dynamic routing

VoiceServer sendNotification() now uses title (agent name) instead of
voiceId for voice resolution. This enables dynamic provider switching
via TTS_PROVIDER env var (google/elevenlabs/macos).

Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName'

Files affected:
- VoiceServer/server.ts (line 425: voiceId → safeTitle)
- PAI/SKILL.md (7 phase curls)
- PAI/Algorithm/v3.7.0.md (2 voice curls)
- agents/Algorithm.md (frontmatter + 3 references)
- PAI/Tools/algorithm.ts (VOICE_ID constant)

Zero hardcoded ElevenLabs IDs remain in the codebase.

* fix: address CodeRabbit review findings on PR #83

Fixes verified against actual code:

Tools path resolution:
- SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/
- ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2
- Both now match GenerateSkillIndex.ts (.opencode/skills/)

Symlink cycle prevention:
- GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths
- ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set

Error handling:
- GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero

Documentation accuracy:
- doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry
- THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode
- THEPLUGINSYSTEM.md: library count 8 → 9 (matches table)
- pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path

Stats accuracy:
- ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills
- pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode

Skipped (verified not applicable):
- PIPELINES.md paths: upstream semantics, both paths correct in context
- VoiceServer shell script hardening: upstream 4.0.3 files, out of scope
- PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture
- THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs

* fix: delete THEHOOKSYSTEM.md and HOOKS.md, complete hook→plugin migration (ADR-001)

- Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md)
- Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md)
- Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename
  hook-system section to plugin-system referencing THEPLUGINSYSTEM.md
- Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology,
  execution flow updated for plugin model (throw Error, not exit codes)
- Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree
- Fix GenerateSkillIndex.ts division by zero when totalSkills == 0

* feat(installer): add anthropic-max preset for Max/Pro OAuth subscription (#84)

* feat(installer): add anthropic-max preset for Max/Pro OAuth subscription

Adds 'anthropic-max' as a built-in installer preset so users with an
existing Anthropic Max or Pro subscription can use PAI-OpenCode without
paying extra for API keys.

## What's new

### Provider preset
- provider-models.ts: add 'anthropic-max' to ProviderName union, PROVIDER_MODELS,
  and PROVIDER_LABELS (label: 'Anthropic Max/Pro (OAuth)')
- Model tiers: haiku-4-5 / sonnet-4-6 / opus-4-6 — same models as 'anthropic'
  but routed through OAuth instead of an API key

### Installer engine (steps-fresh.ts)
- installAnthropicMaxBridge(): new helper function that:
    1. Copies .opencode/plugins/anthropic-max-bridge.js to the local plugins dir
    2. Extracts the OAuth token from macOS Keychain (service: Claude Code-credentials)
    3. Parses claudeAiOauth.{accessToken,refreshToken,expiresAt}
    4. Writes token to ~/.local/share/opencode/auth.json under the 'anthropic' key
    5. Returns success + hours remaining, non-throwing (install continues on failure)
- stepInstallPAI(): call installAnthropicMaxBridge() when provider === 'anthropic-max'
- runFreshInstall(): skip API key prompt for anthropic-max, show Claude CLI check message

### CLI (quick-install.ts)
- --preset anthropic-max works without --api-key (all other presets still require it)
- Inline instructions printed when anthropic-max selected
- Updated help text and examples

### New files
- .opencode/plugins/anthropic-max-bridge.js: 80-line minimal plugin (3 API fixes only)
    Fix 1: system prompt array-of-objects format (prevents HTTP 400)
    Fix 2: anthropic-beta: oauth-2025-04-20 header (prevents HTTP 401)
    Fix 3: Authorization: Bearer <token> instead of x-api-key
- PAI-Install/anthropic-max-refresh.sh: one-command token refresh after expiry
- docs/providers/anthropic-max.md: user-facing setup guide, troubleshooting, tech details

## Usage

Interactive:
  bash install.sh  →  choose 'Anthropic Max/Pro (OAuth)'

Headless:
  bun PAI-Install/cli/quick-install.ts --preset anthropic-max --name 'User'

Token refresh (every ~8-12 hours):
  bash PAI-Install/anthropic-max-refresh.sh

## Notes
- macOS only (requires Keychain access)
- Requires Claude Code CLI installed and authenticated
- Using OAuth tokens in third-party tools may violate Anthropic ToS
- Non-fatal: if Keychain extraction fails, install continues with a warning

* fix(anthropic-max): address CodeRabbit findings + add standalone contrib package

CodeRabbit fixes (PR #84):
- anthropic-max-refresh.sh: use quoted heredoc ('PYEOF') and export tokens
  as env vars to prevent shell injection via token values
- anthropic-max-refresh.sh: add validation for REFRESH_TOKEN (non-empty) and
  EXPIRES_AT (numeric, >0) before writing auth.json
- steps-fresh.ts: fix progress regression — onProgress(92) called after 95%
  steps; corrected to 96
- steps-fresh.ts: replace brittle import.meta.url.replace('file://', '') with
  fileURLToPath(new URL(import.meta.url)) from node:url
- anthropic-max-bridge.js: add explicit comment explaining the authorize block
  returns { type: 'failed' } intentionally (tokens come from auth.json, not
  from an OAuth redirect flow)
- docs/providers/anthropic-max.md: add YAML frontmatter (title, tags,
  published, type, summary) and convert tip block to Obsidian callout syntax

Goal 2 — standalone extractable package:
- contrib/anthropic-max-bridge/ — self-contained, no Bun/PAI required
  - install.sh (bash only, injection-safe heredoc, validates all tokens)
  - refresh-token.sh (same safety fixes as PAI-Install version)
  - plugins/anthropic-max-bridge.js (identical to .opencode/plugins/)
  - README.md (quick-start focused, team-sharing notes)
  - TECHNICAL.md (curl proof, token structure, what we ruled out)
- docs/providers/anthropic-max.md: Option B now points to contrib/ in this
  repo instead of the external jeremy-opencode repository

* fix(anthropic-max): address second-round CodeRabbit findings

- anthropic-max-bridge.js: guard experimental.chat.system.transform against
  output.system being undefined/null/non-array before calling .map(); normalise
  to [] if missing, wrap bare string/object into array, then apply existing
  string->{type,text} conversion
- contrib/anthropic-max-bridge/install.sh: chmod auth.json to 0o600
  (owner read/write only) immediately after writing via os.chmod + stat module;
  mirrors the chmodSync(authFile, 0o600) already present in steps-fresh.ts
- steps-fresh.ts: add zero-case message when hoursRemaining === 0 ('Token
  installed — expired or valid for less than 1 hour. Run
  anthropic-max-refresh.sh now.'); Math.max(0, ...) clamp was already present

* fix(anthropic-max): check Bun.spawn exit code for Keychain lookup

Previously proc.exited was awaited but its return value discarded, so a
non-zero exit from the 'security' command (e.g. item not found) was only
caught via an empty-stdout check. Stdout and stderr are now read concurrently
with proc.exited via Promise.all to avoid pipe deadlock; the exit code is
inspected before accepting keychainJson, and stderr is surfaced in the error
message when present.

* nitpick(steps-fresh): add warning when refresh token is missing

When extracting OAuth tokens from Keychain, warn users if the refresh
token is undefined. This alerts them that they'll need to re-authenticate
with 'claude' when the access token expires.

* feat(oauth): add runtime auto-refresh token bridge (#85)

* feat(oauth): add runtime auto-refresh token bridge

- Add anthropic-token-bridge.js/.ts: checks token every 5 messages,
  auto-refreshes from macOS Keychain when <2h remaining
- Add lib/token-utils.ts + lib/refresh-manager.ts: token lifecycle logic
  (3 strategies: Keychain → claude setup-token → exchange API)
- Add info()/warn()/error() wrappers to file-logger.ts (needed by token bridge)
- Add anthropic-token-bridge.js to contrib/anthropic-max-bridge/plugins/
- Update contrib/install.sh: copies both plugins, updates token refresh note

* docs(contrib): update README + TECHNICAL for auto-refresh token bridge

- README: Token Expiry → auto-refresh primary, manual fallback secondary
- README: File Reference → add anthropic-token-bridge.js entry
- README: install.sh step → mention both plugins copied
- README: token flow diagram → show both plugins
- README: HTTP 401 troubleshooting → mention auto-refresh failed
- TECHNICAL: Token lifetime → replace manual-only with auto-refresh details

* fix(oauth): address CI findings in token bridge

- token-utils.ts: os.homedir() instead of process.env.HOME (avoids unexpanded ~)
- refresh-manager.ts: remove dead promisify(spawn), add execCommand timeout (15s),
  add pragma: allowlist secret on Keychain extractions, mask token in logs
- anthropic-token-bridge.ts/.js: read role from output.message instead of input
- Regenerate compiled .js from fixed TS sources (both plugin + contrib copy)

* fix(oauth): parse claudeAiOauth nested structure from Keychain

The macOS Keychain stores credentials as { claudeAiOauth: { accessToken, refreshToken } }
but the plugin was looking for flat structure. This caused auto-refresh to fail with
hasAccess: false, hasRefresh: false errors, falling back to browser OAuth flow.

Now correctly extracts from nested structure with fallback for legacy formats.

* fix: address CodeRabbit review findings on PR #87

Security:
- auth.json now written with mode 0600 (token-utils, compiled JS, contrib)
- Validate access token and expires field before reporting healthy status
- Refresh scripts check for already-expired Keychain tokens before writing
- Refresh scripts restore chmod 600 after rewriting auth.json
- quick-install.ts fails fast on non-macOS for anthropic-max preset

Correctness:
- Use actual Keychain expiresAt instead of hardcoded 8h TTL
- Token bridge attempts refresh on no_anthropic_config (not just expiry)
- Installer copies anthropic-token-bridge.js alongside the bridge plugin
- contrib anthropic-max-bridge.js gets defensive output.system null-safety
- Remove non-existent FEEDSYSTEM.md from doc-dependencies.json
- GenerateSkillIndex frontmatter regex accepts CRLF line endings
- SkillSearch normalizes PascalCase names for natural language queries

Shell scripts:
- VoiceServer: sed replaces cut -d= -f2 to handle = in values
- VoiceServer: remove unused RED/PLIST_PATH variables
- restart.sh: check exit codes from stop.sh/start.sh

Docs:
- Add language tags to fenced code blocks in contrib README/TECHNICAL

* fix: CodeRabbit round 2 - session-start validation, platform guards, token expiry logic

Session-start improvements:
- GenerateSkillIndex.ts: case-insensitive ALWAYS_LOADED_SKILLS matching
- anthropic-token-bridge: enhanced session-start token validation
- refresh-manager: improved expiresAt handling with null/undefined/past checks

Platform guards:
- VoiceServer/install.sh: curl --fail for notify endpoint
- VoiceServer/uninstall.sh: Darwin platform guard + safe process kill
- steps-fresh.ts: macOS platform gate for anthropic-max + expiresAt validation

Token logic fixes:
- steps-fresh.ts: skip provider API key for anthropic-max in .env
- refresh-manager: AbortController 10s timeout for fetch
- contrib plugin: mirror all fixes

* fix(token-bridge): CRITICAL - synchronous token refresh at session start

The 5-second setTimeout delay caused API calls to fail with expired tokens
before refresh completed. Now refreshes immediately and synchronously
when session starts with an expired/soon-expiring token.

- Changed from setTimeout(5000) to immediate await
- Added fallback async retry on failure
- Applied to: .ts source + compiled .js + contrib plugin

* fix(token-bridge): always sync Keychain→auth.json at session start

Root cause fix for stale token issue: the session-start hook previously
only checked auth.json's `expires` timestamp, which can be valid even
when Anthropic has invalidated the token (e.g. after re-authentication).

Now the hook always reads the Keychain first (source of truth) and writes
it to auth.json before any expiry logic runs. If Keychain and auth.json
tokens differ, a warning is logged and the fresh token is synced.

Fallback path (Keychain unavailable or expired) still triggers full
refreshAnthropicToken() as before.

Also exports extractFromKeychain() from refresh-manager.ts so it can
be used directly in the bridge hook.

* fix: address PR #87 review comments

contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
- Session-start hook now syncs Keychain→auth.json first (same pattern as
  main plugin); extractFromKeychain() result is awaited and wrapped in its
  own try/catch with fileLog/fileLogError; existing refreshAnthropicToken()
  / isRefreshInProgress() fallback path is unchanged

.opencode/PAI/Tools/GenerateSkillIndex.ts
- Duplicate skill name detection now throws an Error instead of warn+continue,
  making the build fail fast with a clear message

.opencode/VoiceServer/install.sh
- API_KEY extraction strips surrounding quotes, inline comments, and
  whitespace before comparing against sentinel values
- ProgramArguments now point to a runtime wrapper (run-server.sh) that
  resolves Bun via PATH at launch time instead of baking in /Users/steffen/.bun/bin/bun
- ELEVENLABS_API_KEY plist entry is only emitted when API_KEY is non-empty
- Fixed sleep 2 with a polling health-check loop (60s timeout, 1s interval)
  that exits with an error if the server never responds

.opencode/VoiceServer/uninstall.sh
- Port cleanup now uses lsof -ti for PID-only output (no header line risk)
  and verifies each PID's command via ps before killing
- Script directory echo now resolves symlinks via readlink -f / realpath

* fix: address second round of PR #87 review comments

.opencode/VoiceServer/install.sh
- ENV_FILE now matches the server's path resolution:
  ${OPENCODE_DIR:-${PAI_DIR:-$HOME/.opencode}}/.env (was $HOME/.env)
- API_KEY extraction uses grep -m1 to avoid concatenating multiple matching
  lines; inline-comment stripping now only removes trailing '#[^"']*$'
  (unquoted comments) so '#' inside a key value is preserved

.opencode/plugins/lib/refresh-manager.ts
.opencode/plugins/anthropic-token-bridge.js
contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
- extractFromKeychain: destructure { stdout, stderr, exitCode } from
  execCommand and pass the real stderr into the error() call (was
  incorrectly passing stdout as stderr)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant